﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace B121 {
    /// <summary>
    /// Errorvalues for packing
    /// </summary>
    [Flags]
    public enum PackingReturnValues {
        // If 0, everything is okay
        ItemYTooBig = 1 << 0, // An item is too long
        ItemXTooBig = 1 << 1, // An item is too wide
        ItemZTooBig = 1 << 2, // An item is too high
        ItemTooHeavy = 1 << 3, // An item is too heavy
        ItemBottomAreaTooBig = 1 << 4,
        ItemsTotalVolumeTooBig = 1 << 5, // Items total volume is too big
        ItemsTotalWeightToBig = 1 << 6, // Items total weight is too big
        ItemDoesNotFit = 1 << 7 // Items could not be packed
    };

    /// <summary>
    /// The template for the packing algorithm
    /// </summary>
    public abstract class PackAlg {
        #region Properties
        /// <summary>
        /// The items to pack
        /// </summary>
        protected List<ItemClass> Items { get; set; }
        /// <summary>
        /// The containers to pack the items into
        /// </summary>
        public List<ContainerClass> Containers { get; set; }
        #endregion

        /// <summary>
        /// Where the packing starts
        /// </summary>
        /// <param name="containers">Containers to pack items into</param>
        /// <param name="items">Items to pack</param>
        /// <returns>0 if packing succeeded, otherwise an combination of error values</returns>
        public PackingReturnValues DoThePacking(List<ContainerClass> containers, List<ItemClass> items) {
            /// Work on a local copy
            Containers = new List<ContainerClass>(containers);
            Items = new List<ItemClass>(items);

            // Prepare items and containers
            PrepareContainers();
            PrepareItems();

            PackingReturnValues errorValues = CheckForBadItems(); // Check if items might fit

            if (errorValues == 0) { // 
                errorValues = AttempToPack(); // Try to pack
            }

            return errorValues;
        }

        #region Prepare Packing
        /// <summary>
        /// Swaps two integer values
        /// </summary>
        /// <param name="a">First value</param>
        /// <param name="b">Second value</param>
        private void swap(ref int a, ref int b) {
            int temp = b;
            b = a;
            a = temp;
        }

        #region Prepare Containers
        /// <summary>
        /// Prepares containers to packing
        /// </summary>
        protected virtual void PrepareContainers() {
            RotateContainers(); // Rotate containers
            Containers.Sort(SortContainers); // Sort containers
        }

        /// <summary>
        /// Rotates the containers so that length >= width >= height
        /// </summary>
        protected virtual void RotateContainers() {
            foreach (ContainerClass c in Containers) {
                int x = c.X, y = c.Y, z = c.Z;

                if (z > x) { swap(ref x, ref z); } // Rotate x<->z if necessary
                if (z > y) { swap(ref z, ref y); } // Rotate y<->z if necessary
                if (x > y) { swap(ref x, ref y); } // Rotate x<->y if necessary

                c.X = x; c.Y = y; c.Z = z;
            }
        }

        /// <summary>
        /// Sorts containers so that length >= width, then by max weight
        /// </summary>
        /// <param name="c1">Container 1</param>
        /// <param name="c2">Container 2</param>
        /// <returns>An int telling which is biggest</returns>
        protected virtual int SortContainers(ContainerClass c1, ContainerClass c2) {
            int yDiff = c2.Y - c1.Y;
            if (yDiff != 0) { return yDiff; } // If one is longer than the other

            int xDiff = c2.X - c1.X;
            if (xDiff != 0) { return xDiff; } // If one is wider than the other

            return c2.MaxWeight - c1.MaxWeight; // Max weight
        }
        #endregion

        #region Prepare Items
        protected virtual void PrepareItems() {
            RotateItems(); // Rotate items
            Items.Sort(SortItems); // Sort item
        }
        /// <summary>
        /// Rotates the items so that length >= width >= height
        /// </summary>
        protected virtual void RotateItems() {
            foreach (ItemClass item in Items) {
                int x = item.X, y = item.Y, z = item.Z;

                if (z > x) { swap(ref x, ref z); } // Rotate x<->z if necessary
                if (z > y) { swap(ref z, ref y); } // Rotate y<->z if necessary
                if (x > y) { swap(ref x, ref y); } // Rotate x<->y if necessary

                item.X = x; item.Y = y; item.Z = z;
            }
        }

        /// <summary>
        /// Sorts items so that length >= width, then by max weight
        /// </summary>
        /// <param name="i1">Item 1</param>
        /// <param name="i2">Item 2</param>
        /// <returns>An int telling which is biggest</returns>
        protected virtual int SortItems(ItemClass i1, ItemClass i2) {
            int yDiff = i2.Y - i1.Y;
            if (yDiff != 0) { return yDiff; } // If one is longer than the other

            int xDiff = i2.X - i1.X;
            if (xDiff != 0) { return xDiff; } // If one is wider than the other

            return i2.Weight - i1.Weight; // Max weight
        }
        #endregion

        #region Check For Bad Items
        /// <summary>
        /// Checks whether the items may fit
        /// </summary>
        /// <returns>0 if items may fit, otherwise false</returns>
        protected virtual PackingReturnValues CheckForBadItems() {
            return (CheckIfASingleItemIsTooBig() | CheckVolumeAreaWeight());
        }

        /// <summary>
        /// Checks if a single item is too big or heavy
        /// </summary>
        /// <returns>0 if all items fit in Containers[0], otherwise an error value</returns>
        protected virtual PackingReturnValues CheckIfASingleItemIsTooBig() {
            PackingReturnValues returnValue = 0; // Assume fit

            // Check on biggest container
            foreach (ItemClass item in Items) { // Check all items
                if (item.X > Containers[0].X) { // Check width
                    returnValue |= PackingReturnValues.ItemXTooBig;
                    //break;
                }
                if (item.Y > Containers[0].Y) { // Check length
                    returnValue |= PackingReturnValues.ItemYTooBig;
                    //break;
                }
                if (item.Z > Containers[0].Z) { // Check height
                    returnValue |= PackingReturnValues.ItemZTooBig;
                    //break;
                }
                if (item.Weight > Containers[0].MaxWeight) { // Check weight
                    returnValue |= PackingReturnValues.ItemTooHeavy;
                    //break;
                }
            }
            return returnValue;
        }

        /// <summary>
        /// Checks if items total volume and weight is less than containers total volume and max weight
        /// </summary>
        /// <returns>0 if items might fit, otherwise an error value</returns>
        protected virtual PackingReturnValues CheckVolumeAreaWeight() {
            int containersVolume = 0, containersMaxWeight = 0, containersBottomArea = 0;
            int itemsVolume = 0, itemsWeight = 0, itemsBottomArea = 0;
            PackingReturnValues errorValues = 0;

            foreach (ContainerClass container in Containers) { // Calculate containers stats
                containersMaxWeight += container.MaxWeight;
                containersBottomArea = container.X * container.Y;
                containersVolume += containersBottomArea * container.Z;
            }

            foreach (ItemClass item in Items) { // Calculate items stats
                itemsWeight += item.Weight;
                itemsBottomArea = item.X * item.Y;
                itemsVolume += itemsBottomArea * item.Z;
            }

            if (itemsVolume > containersVolume) { errorValues |= PackingReturnValues.ItemsTotalVolumeTooBig; } // Items too big?
            if (itemsWeight > containersMaxWeight) { errorValues |= PackingReturnValues.ItemsTotalWeightToBig; } // Items too heavy?
            if (itemsBottomArea > containersBottomArea) { errorValues |= PackingReturnValues.ItemBottomAreaTooBig; } // Too big bottom area?

            return errorValues;
        }
        #endregion
        #endregion

        /// <summary>
        /// Attempts to pack the items
        /// </summary>
        /// <returns>0 if succeeded, otherwise an errorvalue. Must be overriden in derrived class</returns>
        protected abstract PackingReturnValues AttempToPack();
    }
}
